<?php

require_once( "PhpFaces/Context/FacesContext.php" );

/**
 * represents a pluggable mechanism for resolving a top-level variable reference at evaluation time.
 */
class PhpFaces_EL_VariableResolver {
	
	private $logger;
	public function __construct() {
		$this->logger = LoggerManager::getLogger( "PhpFaces.EL.VariableResolver" );
	}

	/**
	 * This does not current support raw strings or anything except single variables.
	 * What we should be doing is parsing the expression into pieces and resolving them
	 * individually if needed.
	 */
	public function &resolveVariable( PhpFaces_Context_FacesContext $facesContext, $expression ) {

		$name = $expression;

		$propertyResolver = $facesContext->getApplication()->getPropertyResolver();
		$context = $facesContext->getELContext();
		
		foreach( $context->getContextNames() as $contextName ) {
			$subContext =& $context->getContext( $contextName );		
			try {
				$value =& $propertyResolver->getValue( $subContext, $name );
				if( $value != null ) {
					return $value;
				}
			} catch( Exception $e ) {
				
			}
		}
		
		// abandon hope all ye who enter here.
		// TODO this should just be a logged warning or somethign
		throw new Exception( "Null value binding" );
	}
}

/**
 * represents a pluggable mechanism for accessing a 
 * "property" of an underlying Java object instance. 
 * Different PropertyResolver implementations can 
 * support property resolution on instances of 
 * different Java classes (such as introspection-based 
 * access to properties of a JavaBeans component, 
 * or get() and put() calls on a java.util.Map instance).
 * 
 * All implementations must respect the rules for 
 * JavaBeans component, java.util.Map, java.util.List 
 * and array instances defined for each method but are 
 * allowed to add custom semantics for other types.
 */
 
class PhpFaces_EL_PropertyResolver {
	
	const TYPE_NUMBER = 1;
	const TYPE_STRING = 2;
	const TYPE_ARRAY = 3;
	const TYPE_METHOD = 4;
	const TYPE_UNKNOWN = 0;
	
	private $logger;
	
	public function __construct() {
		$this->logger = LoggerManager::getLogger( "PhpFaces.EL.PropertyResolver" );
	}

	public function getValue( &$object, $property ) {
		$ref = $this->getVariableReference( $object, $property);
		// purposely dereference it
		$value = $ref->reference;
			
		if( $ref->isCallable ) {
			$value = call_user_func( $ref->reference );
		} 

		return $value;
	}
	
	public function setValue( &$object, $property, $newValue ) {
		$ref = $this->getVariableReference( $object, $property);
		if( $ref->isReadOnly ) {
			throw new Exception( "Can not write this property, it is read only." );
		}
		$value =& $ref->reference;
		$value = $newValue;
	}
	
	public function getType( &$object, $property ) {
		$ref = $this->getVariableReference( $object, $property);
		
		$value =& $ref->reference;
		if( $ref->isCallable ) {
			return self::TYPE_METHOD;
		} else if( is_object( $value ) ) {
			return get_class( $value );
		} else if( is_numeric( $value ) ) {
			return self::TYPE_NUMBER;
		} else if( is_string( $value ) ) {
			return self::TYPE_STRING;
		} else if( is_array( $value ) ) {
			return self::TYPE_ARRAY;
		} else if( is_object( $value ) ) {
			return get_class( $value );
		} else {
			return self::TYPE_UNKNOWN;
		}
	}
	
	public function isReadOnly( &$object, $property ) {
		$ref = $this->getVariableReference( $object, $property);
		return $ref->isReadOnly;
	}
	
	/**
	 * TODO cache the object / property in its final step of recursion
	 */
	protected function getVariableReference( &$object, $property) {
		$expressionParts = split( "\.", $property );
		
		$this->logger->debug( "getVariableReference $property" );
		
		$ref = new PhpFaces_EL_VariableReference();
		$ref->base =& $object; // TODO do we want to set this to the last level?
		$ref->property = $property;
		
		$ref->isCallable = false;
		$ref->isAccessor = false;
		$ref->isReadOnly = true;
		
		$ref->reference =& $object;
		
		$numberOfParts = count($expressionParts);
		for( $i = 0; $i < $numberOfParts; $i++ ) {
			$partName = $expressionParts[$i];  
			if( is_array( $ref->reference ) && array_key_exists( $partName, $ref->reference ) ) {
				$ref->reference =& $ref->reference[$partName];
				$ref->isCallable = false;
				$ref->isAccessor = false;
				$ref->isReadOnly = false;
			} else if( is_object( $ref->reference ) && property_exists( $ref->reference, $partName ) ) {
				$ref->reference =& $ref->reference->$partName;
				$ref->isCallable = false;
				$ref->isAccessor = false;
				$ref->isReadOnly = false;
			} else if( is_object( $ref->reference ) && method_exists( $ref->reference, "get".$partName ) ) {
				$accessor = "get".$partName;
				$ref->isCallable = false;
				$ref->isAccessor = true;
				$ref->isReadOnly = !method_exists( $ref->reference, "set".$partName );
				
				$ref->reference =& $ref->reference->$accessor();
			} else if( is_object( $ref->reference ) && method_exists( $ref->reference, $partName ) ) {
				// TODO do we actually want to invoke the method if it was the last in the chain?
				$functionRef = array( &$ref->reference, $partName);
				if( $i == $numberOfParts - 1 ) {
					$ref->reference =& $functionRef;
				} else {
					$ref->reference =& call_user_func($functionRef);
				}
				$ref->isCallable = true;
				$ref->isAccessor = false;
				$ref->isReadOnly = true;
			} else {
				throw new PhpFaces_EL_VariableNotFoundException( $property );
			}
		}
		
		if( $ref->reference != null ) {
			return $ref;
		}
	}
	
}


/**
 * A class used internally by PropertyResolver to communicate
 * information about a resolved property
 */
class PhpFaces_EL_VariableReference {
	public $isCallable;
	public $isAccessor;
	public $isReadOnly;
	
	public $base;
	public $property;
	public $reference;
}

class PhpFaces_EL_ELContext {
	private $contexts;
	private $resolver;
	
	public function __construct() {
		$this->resolver = new PhpFaces_EL_VariableResolver();
	}
	
	public function putContext( $key, &$context ) {
		$this->contexts[ $key ] = $context;
	}
	
	public function &getContext( $key ) {
		return $this->contexts[ $key ];
	}
	
	// this is not in the spec
	public function getContextNames() {
		return array_keys( $this->contexts );
	}
	
	public function getVariableMapper() {
		return null;
	}
	
	public function getELResolver() {
		return $this->resolver;
	}
}

class PhpFaces_EL_Binding {
	
	protected $expression;
	
	public function __construct( $expression ) {
		$this->expression = $expression;
	}
	
	public function getExpressionString() {
		return $this->expression;
	}

	public function isLiteralText() {
		return false;
	}
	
	protected function resolveToBaseAndProperty(  PhpFaces_Context_FacesContext $facesContext ) {
		$exp = el_to_path( $this->getExpressionString() );
		
		$index = strrpos( $exp, "." );
		
		if( $index === false ) {
			return array( $exp );
		}
		
		$baseExp = substr( $exp, 0, $index );
		$propExp = substr( $exp, $index+1 );

		$base = $facesContext->getApplication()->getVariableResolver()->resolveVariable( $facesContext, $baseExp );
		
		return array( $base, $propExp );
	}
}

class PhpFaces_EL_ValueBinding extends PhpFaces_EL_Binding {

	protected $expectedType;
	
	public function __construct( $expression, $expectedType ) {
		parent::__construct( $expression );
	}
	
	public function getValue( PhpFaces_Context_FacesContext $facesContext ) {
		list( $base, $property ) = $this->resolveToBaseAndProperty( $facesContext );

		$propertyResolver = $facesContext->getApplication()->getPropertyResolver();
		$value = $propertyResolver->getValue( $base, $property );
		
		return $value;
		
	}
	
	public function setValue( PhpFaces_Context_FacesContext $facesContext, $value ) {
		
	}
	
	public function isReadOnly( PhpFaces_Context_FacesContext $facesContext ) {
		list( $base, $property ) = $this->resolveToBaseAndProperty( $facesContext );

		$propertyResolver = $facesContext->getApplication()->getPropertyResolver();
		$value = $propertyResolver->isReadOnly( $base, $property );
		
		return $value;
	}
	
	public function getType( PhpFaces_Context_FacesContext $facesContext ) {
		list( $base, $property ) = $this->resolveToBaseAndProperty( $facesContext );

		$propertyResolver = $facesContext->getApplication()->getPropertyResolver();
		$type = $propertyResolver->getType( $base, $property );
		
		return $type;
	}
	
	public function getExprectedType( PhpFaces_Context_FacesContext $context ) {
		return $this->expectedType;
	}
}


class PhpFaces_EL_MethodBinding extends PhpFaces_EL_Binding {
	public function getMethodInfo( PhpFaces_Context_FacesContext $context ) {
		
	}
	
	public function invoke( PhpFaces_Context_FacesContext $context, $params ) {
		
	}
}

class PhpFaces_EL_VariableNotFoundException extends Exception {
	function __construct( $variable ) {
		parent::__construct( "Could not resolve '$variable'");
	}
}

/**
 * Extract an EL from its prefix/suffix #{expression}
 * @param $el string the EL, including brackets, to resolve
 * @return string the name, minus brackets
 */
function el_to_path( $el ) {
	if( preg_match( PhpFaces_Context_FacesContext::EL_VARIABLE_PATTERN, $el ) ) {
		preg_match( PhpFaces_Context_FacesContext::EL_VARIABLE_PATTERN, $el, $m );
		
		//echo "resolve $el as $m[2]\n";
		
		return $m[2];
	}
}

?>